package sxe.core.cs511.team2_2;

import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.File;
import java.io.IOException;
import java.util.*;

import javax.imageio.ImageIO;
/** OVERVIEW:  This class is responsible for detecting blobs in frames.
 *  The abstraction function is AF(c) = {initial frame window, expected frame values, current frame values, current frame detections}.
 *  The representation invariant is that there will never be any expected frame values if no frames have been processed yet.  
 *  Also, there will never be any current frame detections if the initial frame window has not been surpassed yet. 
 * @author all
 *
 */
public class BlobDetector {
	
	private int initialWindow = 0; //the number of frames used for building background model in the initial phase of detection
	private int framecount=0; //the frame count starting from the time the blob detector was created
	private double intensityThresh = 35; //the threshold on intensity change used to declare a foreground pixel detection
	private float[][][] background = null; // x,y,rgb
	private int[][] detections = null; // x,y, two dimensional array containing the foreground-background mask
	private int[][]labelledDetections; // two dimensional array containing the component labels 
	private int[][][] current=null; // x,y,rgb //image raw data
	private int height=0; //height of image frame
	private int width=0; //width of image frame
	private boolean create_descriptors; //boolean flag set if descriptions of blobs are to be created 
	//filtering parameters for person objects
	//maxSize, maxAspectRatio, maxAspectRatio are parameters used to filter out unwanted blobs that do not appear to correspond to people
	private int minSize = 300; //maximum size of blob in pixels
	private double maxAspectRatio = 4; //max aspect ratio for blobs corresponding to full view of a person
	private double minAspectRatio = 1; //min aspect ration of blobs corresponding to full view of a person
	private Hashtable<Integer, Vector<Integer>> equivTable = null;
        private int max_label;
        
	/** REQUIRES:  iWindow must be a non-negative number.
	 *  EFFECTS:  This constructor creates a new BlobDetector Object, sets the initial window and the create_descriptors flag to the value specified
	 *  MODIFIES:  this.initialWindow, this.createdescriptions
	 * @param iWindow the initial number of frames for which a background model will be created before any detections are made
	 */
	public BlobDetector(int iWindow, boolean create_descriptions){
		initialWindow = iWindow;
		create_descriptors=create_descriptions;
	}
	/** REQUIRES:  iWindow must be a non-negative number.
	 *  EFFECTS:  This constructor creates a new BlobDetector Object, sets the initial window to the value specified and the create_descriptors flag to true
	 *  MODIFIES:  this.iWindow, this.createdescriptions
	 * @param iWindow the initial number of frames for which a background model will be created before any detections are made
	 */
	public BlobDetector(int iWindow){
		initialWindow = iWindow;
		create_descriptors=true;
	}
	
	/**REQUIRES:  frame is non-null 
	 * EFFECTS:  detects blobs in the given frame
	 * MODIFIES:  this
	 * @param frame the frame to process
	 * @return a BlobBag containing all detected blobs
	 */
	public BlobBag detectBlobs(BufferedImage frame){
		boolean process=true; //flag used to determine if this frame will be processed to detect blobs or if it will be used to build background model
		
		if(framecount<initialWindow)
		{
			if(framecount==0) initialize(frame.getRaster().getHeight(), frame.getRaster().getWidth());
			process=false;
			
		}
		framecount++;
		readFrame(frame);
		if(process) 
			threshold();
		
		buildBackground(frame);
		
		if(process){ 
			BlobBag bag = connectedComponent();
			this.removeExtraBlobs(bag);
			return bag;
		}
		else return null;
	}
	
	/**REQUIRES:  h and w to be non-negative 
	 * EFFECTS:  Initializes object for processing frames of the specified size
	 * MODIFIES:  this.width, this.height, this.background, this.detections, this.current
	 * 
	 * @param h the height of the frames
	 * @param w the width of the frames
	 */
	public void initialize(int h, int w)
	{
		width=w;
		height=h;
		background=new float[height][width][3];
		detections=new int[height][width];
		current=new int[height][width][3];
		
	}
	
	/**REQUIRES:  initialize has already been called on this object, frame must be non-null 
	 * EFFECTS: reads in a frame for processing
	 * MODIFIES:  this.current
	 * @param frame the frame to be read in
	 */
	public void readFrame(BufferedImage frame){
		Raster r = frame.getRaster();
		
		//copy image into current array
		for(int i=r.getMinX(), m=0; i<r.getWidth(); i++, m++){
			for(int j=r.getMinY(), n=0; j<r.getHeight(); j++, n++){
				
				current[n][m][0] = (frame.getRGB(i, j)>>16) & 0xff; //r
				current[n][m][1] = (frame.getRGB(i, j)>>8) & 0xff; //g
				current[n][m][2] = frame.getRGB(i, j) & 0xff; //b
			}
		}
		
	}
	
	/**REQUIRES:  frame must be non-null 
	 * EFFECTS:  builds a background model for average pixel values
	 * MODIFIES:  this.background
	 * @param frame the frame to process
	 */
	private void buildBackground(BufferedImage frame){
		
		float alpha = (float).10;
		
		if (framecount == 1){
			//fill background with initial frame values
			for(int m=0; m<width; m++){
				for(int n=0; n<height; n++){
					background[n][m][0] = current[n][m][0];
					background[n][m][1] = current[n][m][1];
					background[n][m][2] = current[n][m][2];
				}
			}
		}
		else{
			for(int m=0; m<width; m++){
				for(int n=0; n<height; n++){
					if (detections[n][m]==0){
					background[n][m][0] = current[n][m][0]*alpha + background[n][m][0]*(1-alpha);
					background[n][m][1] = current[n][m][1]*alpha + background[n][m][1]*(1-alpha);
					background[n][m][2] = current[n][m][2]*alpha + background[n][m][2]*(1-alpha);
					}
				}
			}
		}	
	}
	
	/**REQUIRES:  buildBackground has already been called at least once, readFrame has already been called for this frame. 
	 * EFFECTS:  build detections map based on background and threshold
	 * MODIFIES:  this.detections
	 */
	private void threshold(){
		for(int j=0;j<height;++j)
		{
			for(int i=0;i<width;++i)
			{
				if(Math.abs(background[j][i][0]-current[j][i][0])>=intensityThresh
				 || Math.abs(background[j][i][1]-current[j][i][1])>=intensityThresh
				 || Math.abs(background[j][i][2]-current[j][i][2])>=intensityThresh)
				{
					detections[j][i]=255;
				}
				else
				{
					detections[j][i]=0;
				}
			}
		}
		
	}

	/**REQUIRES:  (1)threshold has already been called for the current frame. 
	 * (2) detections array should be in the following format: 0 and 255 values (0 is no detection, 255 is detection)
	 * EFFECTS:  creates and populates BlobBag based on detections map by doing a connected components analysis. returns a BlogBag object containing all detected Blobs
	 * MODIFIES:  this.labelledDetections
	 *  @return a BlobBag object containing all the detected blobs
	 */
	private BlobBag connectedComponent(){
		//uses the bufferedImage detections.
		//eight neighbourhood connected components
		//expects a binary input image, 0 and 255 values (0 is no detection, 255 is detection
		//1 will be used in the detections image as a visited flag to stop recursion
                equivTable = new Hashtable<Integer, Vector<Integer>>();
		labelledDetections = new int[height][width];
		max_label=0;
		for(int i=0; i<height; i++){ 
		  for(int j=0; j<width; j++){
		    if(detections[i][j]==0)
		       labelledDetections[i][j]=255; //255 is for background in output
		    if(detections[i][j]!=1 && detections[i][j]!=0/*>255*/){
		      SequentialLabel(i, j);
		      //max_label++;
		    }
		  }
		}
		
                ReplaceEquivalentLabels();
		System.out.println("Max label:"+ max_label);
		BlobBag blobBag = new BlobBag();
		int size_threshold = 10;
		int max_x=-1, max_y=-1, min_x=width, min_y=height;
		 //at this point labelledDetections should contain labelled connected components
		 //iterate through all components upto max_label
		for(int k=0; k<max_label; k++){
			int size=0;
			max_x=-1; max_y=-1; min_x=width; min_y=height;
			for(int i=0; i<height; i++){
			   for(int j=0; j<width; j++){
				   if(labelledDetections[i][j]==k){
					   size++;
					   //find the maxx, maxy, minx, miny of the component
					   if(i<min_y){
						   min_y = i;
					   }
					   if(i>max_y){
						   max_y = i;
					   }
					   if(j<min_x){
						   min_x = j;
					   }
					   if(j>max_x){
						   max_x = j;
					   }
				   }//end of labelled..if
			   }   //end of for
			}//enf of for 
		 if(size<size_threshold){ //if the size is below a threshold, ignore
			 continue;
		 }
		 else{ 
			 //create a new blob, add to BlogBag
			 Blob nBlob;
			 java.awt.Rectangle rect=new java.awt.Rectangle(min_x, min_y, max_x - min_x, max_y - min_y);
			 if(create_descriptors){
				 BlobDescription desc=new BlobDescription(current,labelledDetections,k,rect);
			 	 nBlob=new Blob(desc,rect,blobBag.getStamp());
			 }
			 //Blob nBlob = new Blob(min_x, min_y, max_y - min_y, max_x - min_x);
			 else nBlob=new Blob(rect);
			 blobBag.add(nBlob);	 
		 }
		}//end of label loop
		return blobBag;
	}
	
        /** This helper method performs a connected component analysis sequentially. 
         */
        private void SequentialLabel(int i, int j){
            detections[i][j]=1; //visited tag
            
            //handle boundary conditions 
            
            // detection is in upper-left corner of image 
            if (i==0 && j==0){
                max_label++;
                labelledDetections[i][j]=max_label;
                Vector<Integer> newVec = new Vector<Integer>();
                equivTable.put(new Integer(max_label), newVec);
                return;
            }
            
            // detection is in top row of image
            if (i == 0){
                if (detections[i][j-1] != 0)
                    labelledDetections[i][j]=labelledDetections[i][j-1]; 
                else{
                    max_label++;
                    labelledDetections[i][j]=max_label;
                    Vector<Integer> newVec = new Vector<Integer>();
                    equivTable.put(new Integer(max_label), newVec);
                }
                return;
            }
            
            // detection is in left column of image
            if (j==0){
                if (detections[i-1][j] != 0)
                    labelledDetections[i][j]=labelledDetections[i-1][j]; 
                else{
                    max_label++;
                    labelledDetections[i][j]=max_label;
                    Vector<Integer> newVec = new Vector<Integer>();
                    equivTable.put(new Integer(max_label), newVec);
                }
                return;
            }
                
            
            // if only one of upper and left neighbors is labeled, copy the label
            if (detections[i-1][j] != 0 && detections[i][j-1] == 0)
                labelledDetections[i][j]=labelledDetections[i-1][j];
            else if (detections[i-1][j] == 0 && detections[i][j-1] != 0)
                labelledDetections[i][j]=labelledDetections[i][j-1];    
            
            // if both have the same label, copy the label
            else if (detections[i-1][j] != 0 && labelledDetections[i-1][j] == labelledDetections[i][j-1])
                labelledDetections[i][j]=labelledDetections[i][j-1];    
            
            // if both have different labels, copy the upper's label and make note in equivTable
            else if (detections[i-1][j] != 0 && detections[i][j-1] != 0 &&
                    labelledDetections[i-1][j] != labelledDetections[i][j-1]){
                Vector<Integer> vec1 = equivTable.get(new Integer(labelledDetections[i-1][j]));
                vec1.add(new Integer(labelledDetections[i][j-1]));
                Vector<Integer> vec2 = equivTable.get(new Integer(labelledDetections[i][j-1]));
                vec2.add(new Integer(labelledDetections[i-1][j]));
                labelledDetections[i][j]=labelledDetections[i-1][j];    
            }
            
            // otherwise, assign a new label and make entry in equivTable
            else{
                max_label++;
                labelledDetections[i][j]=max_label;
                Vector<Integer> newVec = new Vector<Integer>();
                equivTable.put(new Integer(max_label), newVec);
            }
        }
        
        /** This helper method makes a second pass through the image and replaces equivalent
         * labels so that each connected component has one unique label.
         */ 
        private void ReplaceEquivalentLabels(){
            Enumeration enu = equivTable.keys();
            while (enu.hasMoreElements()){
                int alabel = ((Integer)enu.nextElement()).intValue();
                Vector equivList = (Vector)equivTable.get(new Integer(alabel));
                if (equivList.size() > 0){
                    int minLabel = alabel;
                    for (int i = 0; i<equivList.size(); i++){
                        int oneequiv = ((Integer)equivList.get(i)).intValue();
                        if (oneequiv < minLabel)
                            minLabel = oneequiv;
                    }
                    if (minLabel != alabel)
                        ReplaceLabel(alabel, minLabel);
                }
            }
        }
        
        private void ReplaceLabel(int lookFor, int replaceWith){
            for(int i=0; i<height; i++){ 
		  for(int j=0; j<width; j++){
		    if(labelledDetections[i][j] == lookFor)
                        labelledDetections[i][j] = replaceWith;
                  }
            }
        }
        
	//helper function: recursive function for labelling components
	private  void Recursivelabel(int i, int j, int lb){
		 //System.out.println("i="+ i + " j" + j + "label " + lb);
		  detections[i][j]=1; //visited tag
		  labelledDetections[i][j]=lb;
		  //printf("i=%i,j=%i,lb=%i", i, j, lb);

		  //look at eight neighbours and if a detection is found recurse to that pixel to continue labelling
		  if((i-1<height && j-1<width && i-1>=0 && j-1>=0)){ //checking for image bounds{
			  if(detections[i-1][j-1]!= 1 && detections[i-1][j-1]!=0/*>200*/){
				  Recursivelabel( i-1, j-1, lb); 
			  }
		  }
		  if((i-1<height && j<width && i-1>=0 && j>=0)){
			  if( detections[i-1][j]!= 1 && detections[i-1][j]!=0/*>200*/ ){
				  Recursivelabel( i-1, j, lb);
			  }
		  }
		  if((i-1<height && j+1<width && i-1>=0 && j+1>=0)){
			  if(  detections[i-1][j+1]!= 1 && detections[i-1][j+1]!=0/*>200*/){
                                  Recursivelabel( i-1, j+1, lb);
			  }
		  }
		  if((i<height && j-1<width && i>=0 && j-1>=0)){
			  if( detections[i][j-1]!= 1 && detections[i][j-1]!=0/*>200*/ ){
				  Recursivelabel( i, j-1, lb);
			  }
		  }
		  if((i<height && j+1<width && i>=0 && j+1>=0)){
			  if( detections[i][j+1]!= 1 && detections[i][j+1]!=0/*>200*/ ){
                                  Recursivelabel(i, j+1, lb);
			  }
		  }
		  if((i+1<height && j-1<width && i+1>=0 && j-1>=0 )){		  
			  if( detections[i+1][j-1]!= 1 && detections[i+1][j-1]!=0/*>200*/){
                                  Recursivelabel( i+1, j-1, lb);
			  }
		  }
		  if((i+1<height && j<width && i+1>=0 && j>=0)){
			  if(  detections[i+1][j]!= 1 && detections[i+1][j]!=0/*>200*/ ){
                                  Recursivelabel( i+1, j, lb);
			  }
		  }
		  if((i+1<height && j+1<width &&i+1>=0 && j+1>=0)){
			  if(  detections[i+1][j+1]!= 1 && detections[i+1][j+1]!=0/*>200*/){
                                  Recursivelabel( i+1, j+1, lb);
			  }
		  }
		}
	
	
	/** EFFECTS:  draws bounding boxes for all blob objects found in the current frame.
	 * 
	 * @param b blogbag containing the bounding box locations and sizes
	 * @return BufferentImage containing original frame and bounding boxes are drawn in.
	 *  
	 */
	public BufferedImage drawBoundingBoxes(BlobBag b){
		BufferedImage bimageBB = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		int [][]imageBB = new int[height][width];

		//copy current into imageBB
		for(int i=0; i<height; i++)
			for(int j=0; j<width; j++)
				imageBB[i][j] = (current[i][j][0] << 16) | (current[i][j][1] << 8) | current[i][j][2];
			
		
		//image is the image in which to draw boxes
		java.util.Iterator<Blob> it = b.listIterator();
		  while (it.hasNext()) {
		    Blob bl = (Blob)it.next();
		    int bx = bl.getX();
		    int by = bl.getY();
		    int bh = bl.getHeight();
		    int bw = bl.getWidth();
		    //top horizontal line
		    for(int i=bx; i<=bx+bw; i++)
		    	imageBB[by][i] = 0xff0000;
		    //bottom horizontal line
		    for(int i=bx; i<=bx+bw; i++)
		    	imageBB[by+bh][i]=0xff0000;
		    //left line
		    for(int i=by; i<=by+bh; i++)
		    	imageBB[i][bx]=0xff0000;
		    //right line
		    for(int i=by; i<=by+bh; i++)
		    	imageBB[i][bx+bw]=0xff0000;
		    // this can be used to show blob pixels in green
		    /*for(int i=by; i<=by+bh; ++i)
		    	for(int j=bx; j<bx+bw; ++j)
		    		if(this.labelledDetections[i][j]!=255)
		    			imageBB[i][j]=0x00ff00;*/
		  }
		  
			
		  Raster r = bimageBB.getRaster(); 
		  for(int i=r.getMinX(), m=0; i<r.getWidth(); i++, m++){
			  for(int j=r.getMinY(), n=0; j<r.getHeight(); j++, n++){
				  bimageBB.setRGB(i, j, imageBB[n][m]); 
			  }
		  }

		  return bimageBB;
	}

	/** EFFECTS:  returns the BufferedImage corresponding to the background
	 * 
	 */
	public BufferedImage getBackground(){
		BufferedImage bimageBB = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		int [][]imageBB = new int[height][width];

		//copy background into imageBB
		for(int i=0; i<height; i++)
			for(int j=0; j<width; j++)
				imageBB[i][j] = ((int)(background[i][j][0]) << 16) | 
								((int)(background[i][j][1]) << 8) | 
								((int)background[i][j][2]);
		  
			
		  Raster r = bimageBB.getRaster(); 
		  for(int i=r.getMinX(), m=0; i<r.getWidth(); i++, m++){
			  for(int j=r.getMinY(), n=0; j<r.getHeight(); j++, n++){
				  bimageBB.setRGB(i, j, imageBB[n][m]); 
			  }
		  }

		  return bimageBB;
	}


	/** EFFECTS:  performs post processing on detected blobs to remove ones that do on look like people, based on the aspect ratio and size
	 *  MODIFIES: b (the blobbag passed to it)
	 * @param b blogbag containing the bounding box locations and sizes
	 *  
	 */
	public void removeExtraBlobs(BlobBag b){
		//image is the image in which to draw boxes
		java.util.Iterator<Blob> it = b.listIterator();
		  while (it.hasNext()) {
		    Blob bl = it.next();
		    int bx = bl.getX();
		    int by = bl.getY();
		    int bh = bl.getHeight();
		    int bw = bl.getWidth();
		    
		    //aspect ratio and size checks to filter extra blobs
		    double aspectRatio = (double)bh/(double)bw; 
		    if(aspectRatio < minAspectRatio || aspectRatio > maxAspectRatio || bh*bw<minSize){
		    	//int index = it.previousIndex() +1 ; //index of this blog
		    	//b.deleteBlob(bl);
		    	it.remove();
		    }
		  }		
	}

	
	/** EFFECTS:  Provides a unit-test of the methods in this class using pre-recorded image data.
	 * 
	 * @param args The arguments are not used.
	 */
	public static void main(String args[]){
		System.out.println("Hello World");
		//testing the connected components methods
		//open image file
		File f = new File("C:\\Documents and Settings\\lbasile\\Desktop\\data1.png");
		//Image im = null;
		BufferedImage bi = null;
		
		try{
			bi = ImageIO.read(f);
			System.out.println(bi.toString());
			
		}catch(IOException e){
			System.out.println("Unable to open file " + e.getMessage());
		}
		
		File f1 = new File("C:\\Documents and Settings\\lbasile\\Desktop\\data2.png");
		//Image im = null;
		BufferedImage bi1 = null;
		
		try{
			bi1 = ImageIO.read(f1);
			
		}catch(IOException e){
			System.out.println("Unable to open file " + e.getMessage());
		}

				
		//copy into detections array
		BlobDetector blobDetector = new BlobDetector(1);
		
		blobDetector.detectBlobs(bi); //this call will create background model and return null
		
		BlobBag b = blobDetector.detectBlobs(bi1); //called with object of interest  
		System.out.println("blogbagsize= "+ b.size());
		
		//draw boxes to show the bloglocations
		BufferedImage imout = blobDetector.drawBoundingBoxes(b);
		File f2 = new File("C:\\Documents and Settings\\lbasile\\Desktop\\result.png");
		try{
			ImageIO.write(imout, "png", f2);
		}catch(IOException e){
			System.out.println("Unable to write file " + e.getMessage());
		}
	}
	
}
